package com.snowyee.yudada.scoring.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.snowyee.yudada.manager.AiManager;
import com.snowyee.yudada.model.dto.question.QuestionAnswerDTO;
import com.snowyee.yudada.model.dto.question.QuestionContentDTO;
import com.snowyee.yudada.model.entity.App;
import com.snowyee.yudada.model.entity.Question;
import com.snowyee.yudada.model.entity.UserAnswer;
import com.snowyee.yudada.model.vo.QuestionVO;
import com.snowyee.yudada.scoring.ScoringStrategy;
import com.snowyee.yudada.scoring.ScoringStrategyConfig;
import com.snowyee.yudada.service.QuestionService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * AI 测评类应用评分策略
 */
@ScoringStrategyConfig(appType = 1, scoringStrategy = 1)
public class AiTestScoringStrategyImpl implements ScoringStrategy {

    @Resource
    private QuestionService questionService;

    @Resource
    private AiManager aiManager;

    @Resource
    private RedissonClient redissonClient;

    // 分布式锁的key
    private static final String AI_ANSWER_LOCK = "AI_ANSWER_LOCK";


    /**
     * 本地缓存
     */
    private final Cache<String, String> answerCacheMap =
            Caffeine.newBuilder().initialCapacity(1024)
                    .expireAfterAccess(5L, TimeUnit.MINUTES)
                    .build();


    /**
     * 构建缓存key
     */
    private String buildCacheKey(Long appId, String choices) {
        return DigestUtil.md5Hex(appId +":"+ choices);
    }
    /**
     * 评分系统prompt
     */
    private static final String AI_TEST_SCORING_SYSTEM_MESSAGE = "你是一位严谨的判题专家，我会给你如下信息：\n" +
            "```\n" +
            "应用名称，\n" +
            "【【【应用描述】】】，\n" +
            "题目和用户回答的列表：格式为 [{\"title\": \"题目\",\"answer\": \"用户回答\"}]\n" +
            "```\n" +
            "\n" +
            "请你根据上述信息，按照以下步骤来对用户进行评价：\n" +
            "1.要求：需要给出一个明确的评价结果，包括评价名称（尽量简短）和评价描述（尽量详细，大于 200 字）\n" +
            "2.严格按照下面的 json 格式输出评价名称和评价描述\n" +
            "```\n" +
            "{\"resultName\": \"评价名称，评价名称必须是MBTI16种人格中的一种\", \"resultDesc\":\"评价描述\"}\n" +
            "```\n" +
            "3.返回格式必须为 JSON 对象";


    @Override
    public UserAnswer doScore(List<String> choices, App app) throws Exception {



        // 1. 根据id查询到题目和题目结果信息
        Long appId = app.getId();

        // 缓存appid+choices+answerJson, 用md5加密后缓存
        String jsonChoices = JSONUtil.toJsonStr(choices);
        String cacheKey = buildCacheKey(appId, jsonChoices);
        /**
         * getIfPresent(cacheKey): 这是一个方法，用于从缓存中获取与 cacheKey 相关联的值。如果缓存中存在该键的值，则返回该值；如果不存在，则返回 null。
         */
        String answerJson = answerCacheMap.getIfPresent(cacheKey);

        // 如果有缓存，直接返回
        if (StrUtil.isNotBlank(answerJson)) {
            // 构造返回值，填充答案对象的属性
            UserAnswer userAnswer = JSONUtil.toBean(answerJson, UserAnswer.class);
            userAnswer.setAppId(appId);
            userAnswer.setAppType(app.getAppType());
            userAnswer.setScoringStrategy(app.getScoringStrategy());
            userAnswer.setChoices(jsonChoices);
            return userAnswer;
        }

        // 定义锁
        RLock lock = redissonClient.getLock(AI_ANSWER_LOCK + cacheKey);

        try{
            // 竞争锁
            /**
             * time（第一个参数，3）：表示在尝试获取锁时的初始等待时间。这里设置为 3。
             * time（第二个参数，15）：表示在尝试获取锁时的最大等待时间。这里设置为 15。
             * unit（第三个参数，TimeUnit.SECONDS）：表示时间单位，这里使用的是秒 (SECONDS)。
             *
             * 初始等待时间 是线程第一次尝试获取锁时的等待时间，可以用来减少无效的等待。
             * 最大等待时间 是线程总共尝试获取锁的最大时间，防止线程无限等待锁的情况。
             *
             * 我做这块的时候，有个问题，设置这3s有什么意义，直接等15s不行吗，为什么要等3s获取不到，再继续等？
             *
             * 原因：
             * 避免长时间阻塞：在某些情况下，线程可能会在尝试获取锁时遇到短暂的阻塞。通过设置一个较短的初始等待时间，线程可以先尝试获取锁。如果在短时间内成功获取锁，那么它就不必一直等待。这样可以减少无效等待，提高系统响应速度。
             *
             * 适应性重试：初始等待时间为线程提供了一个快速重试的机会。如果锁在短时间内可用，线程可以立即继续执行，而不必等待整个最大等待时间。这样可以提高锁获取的成功率，特别是在锁的持有时间较短的情况下。
             *
             */
            boolean res = lock.tryLock(3, 15, TimeUnit.SECONDS);

            // 获取不到锁，直接返回
            if (!res){
                return null;
            }

            Question question = questionService.getOne(
                    Wrappers.lambdaQuery(Question.class)
                            .eq(Question::getAppId, appId)
            );

            QuestionVO questionVO = QuestionVO.objToVo(question);
            List<QuestionContentDTO> questionContent = questionVO.getQuestionContent();


            // 2. 调用AI获取结果
            // 封装 userMessage的prompt
            String userMessage = getAiTestScoringUserMessage(app, questionContent, choices);
            // AI 生成答案
            String result = aiManager.doSyncStableRequest(AI_TEST_SCORING_SYSTEM_MESSAGE, userMessage);

            // 截取需要的JSON信息
            int start = result.indexOf("{");
            int end = result.lastIndexOf("}");
            String resultJson = result.substring(start, end + 1);

            // 缓存结果
            answerCacheMap.put(cacheKey, resultJson);

            // 4. 构造返回值，填充答案对象的属性
            UserAnswer userAnswer = JSONUtil.toBean(resultJson, UserAnswer.class);
            userAnswer.setAppId(appId);
            userAnswer.setAppType(app.getAppType());
            userAnswer.setScoringStrategy(app.getScoringStrategy());
            userAnswer.setChoices(jsonChoices);
            return userAnswer;

        }finally {
            if (lock != null && lock.isLocked()){
                // 为了确保只有持有锁的线程才能释放锁，如果一个线程试图释放它没有持有的锁，可能引发死锁。
                if (lock.isHeldByCurrentThread()){
                    lock.unlock();
                }
            }
        }




    }

    /**
     * 评分用户prompt
     */
    private String getAiTestScoringUserMessage(App app, List<QuestionContentDTO> questionContentDTOList, List<String> choices) {
        StringBuilder userMessage = new StringBuilder();
        userMessage.append(app.getAppName()).append("\n");
        userMessage.append(app.getAppDesc()).append("\n");
        List<QuestionAnswerDTO> questionAnswerDTOList = new ArrayList<>();
        for (int i = 0; i < questionContentDTOList.size(); i++) {
            QuestionAnswerDTO questionAnswerDTO = new QuestionAnswerDTO();
            questionAnswerDTO.setTitle(questionContentDTOList.get(i).getTitle());
            // 我感觉这块有点不对劲，choices.get(i)是【A,B,A,A】这种，不带文字的，而我们喂给AI的prompt应该是
            /**
             * MBTI 性格测试，
             * 【【【快来测测你的 MBTI 性格】】】，
             * [{"title":"你通常更喜欢","answer": "独自工作"}, {"title": "当安排活动时","answer": "更愿意随机应变"}]
             * 这样的格式，应该处理成文字才对
             */
            questionAnswerDTO.setUserAnswer(choices.get(i));
            questionAnswerDTOList.add(questionAnswerDTO);
        }
        userMessage.append(JSONUtil.toJsonStr(questionAnswerDTOList));
        return userMessage.toString();
    }
}
